////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2009 Daniel F. Hanson
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
////////////////////////////////////////////////////////////////////////////////

using System;
using System.Collections.Generic;
#if DEBUG
using System.Diagnostics;
#endif
using System.Reflection;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;

namespace SilverLua.Runtime
{
   /// <summary>
   /// Manages all aspects of SilverLua, including running scripts, storing
   /// the global table, and providing interop between C# and SilverLua
   /// </summary>
   public class VirtualMachine
   {
      private ContentManager _content;
      
      private Table _baseTable = new Table();
      private const string BaseTableName = "base";
      private const string GlobalTableName = "_G";
      private Dictionary<string, Environment> _environments = new Dictionary<string, Environment>();
      
      public string Out = null;
      
      public VirtualMachine(Game game)
      {
         _content = new ContentManager(game.Content.ServiceProvider, @"Content\Scripts");
      }
      
      public void CreateEnvironment(string name)
      {
         if (_environments.ContainsKey(name))
         {
            throw new InvalidOperationException(string.Format("Environment '{0}' already exists", name));
         }
         
         Table globalTable = new Table();
         _baseTable[new Value(name)] = new Value(globalTable);
         globalTable[new Value(BaseTableName)] = new Value(_baseTable);
         globalTable[new Value(GlobalTableName)] = new Value(globalTable);
         globalTable[new Value("assert")] = new Value(typeof(VirtualMachine).GetMethod("Assert", BindingFlags.NonPublic | BindingFlags.Static));
         globalTable[new Value("print")] = new Value(typeof(VirtualMachine).GetMethod("Print", BindingFlags.NonPublic | BindingFlags.Static));
         
         _environments.Add(name, new Environment(this, name, globalTable));
      }
      
      private static void Assert(bool value)
      {
         Debug.Assert(value);
      }

      private static void Print(string message)
      {
         Trace.WriteLine(message);
      }
      
      public Script LoadScript(string envName, string assetName, bool autoStart)
      {
         return GetEnvironment(envName).LoadScript(_content.Load<ScriptBinary>(assetName), autoStart);
      }
      
      public void LoadExposedTypes(Assembly assembly)
      {
         Type[] types = assembly.GetTypes();
         
         foreach (Type type in types)
         {
            object[] attributes = type.GetCustomAttributes(typeof(LuaExposeAttribute), false);
            
            foreach (LuaExposeAttribute attr in attributes)
            {
               ExposeType(type, attr.Rename, attr.Parent, attr.Environment);
            }
         }
      }
      
      public void ExposeType(Type type)
      {
         ExposeType(type, null, null, null);
      }

      public void ExposeType(Type type, string rename, string parent, string environment)
      {
      }

      public void PauseEnvironment(string envName)
      {
         GetEnvironment(envName).IsPaused = true;
      }

      public void ResumeEnvironment(string envName)
      {
         GetEnvironment(envName).IsPaused = false;
      }
      
      public void UpdateEnvironment(string envName, TimeSpan time)
      {
         GetEnvironment(envName).Update(time);
      }
      
      private Environment GetEnvironment(string envName)
      {
         Environment env;

         if (!_environments.TryGetValue(envName, out env))
         {
            throw new InvalidOperationException(string.Format("Environment '{0}' does not exist", envName));
         }

         return env;
      }

      internal void Execute(Instruction instruction, Script script, Environment environment)
      {
         #if DEBUG
         Trace.Write(instruction.Opcode.ToString());
         for (int i = 0; i < instruction.OperandCount; ++i)
         {
            Trace.Write(string.Format("  {0}:{1}", instruction.OperandTypes[i], instruction.OperandValues[i]));
         }
         Trace.WriteLine("");
         #endif
         
         switch (instruction.Opcode)
         {
            case Opcode.Add:                 Add(instruction, script, environment); break;
            case Opcode.Call:                Call(instruction, script, environment); break;
            case Opcode.Concatenate:         Concatenate(instruction, script, environment); break;
            case Opcode.ConditionalJump:     ConditionalJump(instruction, script, environment); break;
            case Opcode.CreateTable:         CreateTable(instruction, script, environment); break;
            case Opcode.Divide:              Divide(instruction, script, environment); break;
            case Opcode.Equal:               Equal(instruction, script, environment); break;
            case Opcode.Exponent:            Exponent(instruction, script, environment); break;
            case Opcode.Function:            Function(instruction, script, environment); break;
            case Opcode.GetGlobal:           GetGlobal(instruction, script, environment); break;
            case Opcode.GetTable:            GetTable(instruction, script, environment); break;
            case Opcode.Jump:                Jump(instruction, script, environment); break;
            case Opcode.Length:              Length(instruction, script, environment); break;
            case Opcode.Less:                Less(instruction, script, environment); break;
            case Opcode.LessEqual:           LessEqual(instruction, script, environment); break;
            case Opcode.Modulo:              Modulo(instruction, script, environment); break;
            case Opcode.Move:                Move(instruction, script, environment); break;
            case Opcode.MoveOrSkip:          MoveOrSkip(instruction, script, environment); break;
            case Opcode.Multiply:            Multiply(instruction, script, environment); break;
            case Opcode.Negate:              Negate(instruction, script, environment); break;
            case Opcode.Not:                 Not(instruction, script, environment); break;
            case Opcode.NotEqual:            NotEqual(instruction, script, environment); break;
            case Opcode.Return:              Return(instruction, script, environment); break;
            case Opcode.SetGlobal:           SetGlobal(instruction, script, environment); break;
            case Opcode.SetTable:            SetTable(instruction, script, environment); break;
            case Opcode.Subtract:            Subtract(instruction, script, environment); break;
            default:                         throw new Exception(string.Format("Unrecognized opcode {0}", (int)instruction.Opcode));
         }
      }
      
      #region Instruction implementations
      private void Add(Instruction instruction, Script script, Environment environment)
      {
         #if DEBUG
         Debug.Assert(instruction.OperandCount == 3);
         Debug.Assert(IsVariable(instruction.OperandTypes[0]));
         #endif

         Value op1 = GetValue(script, instruction.OperandTypes[1], instruction.OperandValues[1]);
         Value op2 = GetValue(script, instruction.OperandTypes[2], instruction.OperandValues[2]);

         Value dest = GetValue(script, instruction.OperandTypes[0], instruction.OperandValues[0]);
         dest.SetValue(op1);
         
         PrepareForArithmetic(dest);
         dest.Add(op2);
      }
      
      private static object[] GetFunctionArguments(Script script,
                                                   int argBegin,
                                                   int argCount,
                                                   string methodName,
                                                   ParameterInfo[] methodParams,
                                                   bool isStatic,
                                                   ref object self)
      {
         if (!isStatic)
         {
            if (argCount < 1)
            {
               throw new Exception("Cannot invoke non-static member without self parameter");
            }
            else
            {
               self = script.RegisterStack[script.RegisterStack.Count - 1][argBegin].GetHostObject();
               ++argBegin;
               --argCount;
            }
         }

         if (argCount != methodParams.Length)
         {
            throw new Exception(string.Format("{0} expects {1} arguments, received {2}", methodName, methodParams.Length, argCount));
         }

         object[] args = new object[argCount];

         for (int i = 0; i < argCount; ++i)
         {
            Value arg = script.RegisterStack[script.RegisterStack.Count - 1][argBegin + i];

            if (methodParams[i].ParameterType == typeof(int))
            {
               args[i] = arg.GetInteger();
            }
            else if (methodParams[i].ParameterType == typeof(float))
            {
               args[i] = arg.GetFloat();
            }
            else if (methodParams[i].ParameterType == typeof(string))
            {
               args[i] = arg.GetString();
            }
            else if (methodParams[i].ParameterType == typeof(bool))
            {
               args[i] = arg.GetBoolean();
            }
            else if (arg.Type == ValueType.HostObject)
            {
               args[i] = arg.GetHostObject();
            }
            else
            {
               throw new Exception(string.Format("Cannot pass parameter of type {0}", arg.Type.ToString()));
            }
         }

         return args;
      }
      
      private void Call(Instruction instruction, Script script, Environment environment)
      {
         #if DEBUG
         Debug.Assert(instruction.OperandCount == 3);
         Debug.Assert(IsVariable(instruction.OperandTypes[0]));
         Debug.Assert(instruction.OperandTypes[1] == OperandType.IntegerLiteral);
         Debug.Assert(instruction.OperandTypes[2] == OperandType.IntegerLiteral);
         #endif

         Value funcValue = GetValue(script, instruction.OperandTypes[0], instruction.OperandValues[0]);
         int argBegin = instruction.OperandValues[1];
         int argCount = instruction.OperandValues[2];

         if (funcValue.Type == ValueType.Function)
         {
            Function func = funcValue.GetFunction();
            Value[] funcParams = new Value[func.ParameterCount];
            
            for (int i = 0; i < func.ParameterCount; ++i)
            {
               if (i < argCount)
               {
                  funcParams[i] = new Value(script.RegisterStack[script.RegisterStack.Count - 1][argBegin+i]);
               }
               else
               {
                  funcParams[i] = new Value();
               }
            }
            
            script.PushChunk(func.Script, func.ChunkIndex, funcParams, func.Upvalues);
         }
         else if (funcValue.Type == ValueType.HostMethod)
         {
            HostMethod function = funcValue.GetHostMethod();
            MethodInfo method = function.MethodInfo;
            ParameterInfo[] methodParams = function.ParameterInfo;
            
            object self = null;
            object[] args = GetFunctionArguments(script, argBegin, argCount, method.Name, methodParams, method.IsStatic, ref self);

            object retValue = method.Invoke(self, args);
            if (retValue == null)
            {
               script.ReturnNil();
            }
            else if (!script.ReturnValue(retValue, method.ReturnType))
            {
               script.ReturnHostObject(retValue,
                                       environment.GeneratePrototype(method.ReturnType, retValue));
            }
         }
         else if (funcValue.Type == ValueType.HostConstructor)
         {
            HostConstructor function = funcValue.GetHostConstructor();
            ConstructorInfo ctor = function.CtorInfo;
            ParameterInfo[] ctorParams = function.ParameterInfo;
            
            object type = null;
            object[] args = GetFunctionArguments(script, argBegin, argCount, ctor.Name, ctorParams, false, ref type);
            
            object hostObject = ctor.Invoke(args);
            script.ReturnHostObject(hostObject,
                                    environment.GeneratePrototype(ctor.DeclaringType, hostObject));
         }
         else
         {
            throw new Exception(string.Format("Cannot invoke function call on {0}", funcValue.Type.ToString()));
         }
      }

      private void Concatenate(Instruction instruction, Script script, Environment environment)
      {
         #if DEBUG
         Debug.Assert(instruction.OperandCount == 3);
         Debug.Assert(IsVariable(instruction.OperandTypes[0]));
         #endif

         Value op1 = GetValue(script, instruction.OperandTypes[1], instruction.OperandValues[1]);
         Value op2 = GetValue(script, instruction.OperandTypes[2], instruction.OperandValues[2]);

         Value dest = GetValue(script, instruction.OperandTypes[0], instruction.OperandValues[0]);
         dest.SetValue(op1);
         
         dest.Concatenate(op2);
      }

      private void ConditionalJump(Instruction instruction, Script script, Environment environment)
      {
         #if DEBUG
         Debug.Assert(instruction.OperandCount == 3);
         Debug.Assert(instruction.OperandTypes[0] == OperandType.IntegerLiteral);
         Debug.Assert(IsVariable(instruction.OperandTypes[1]));
         Debug.Assert(instruction.OperandTypes[2] == OperandType.BooleanLiteral);
         #endif
         
         Value sourceValue = GetValue(script, instruction.OperandTypes[1], instruction.OperandValues[1]);
         bool op1 = sourceValue.GetBoolean();
         bool op2 = instruction.OperandValues[2] != 0;
         
         if (op1 == op2)
         {
            script.Jump(instruction.OperandValues[0]);
         }
      }
      
      private void CreateTable(Instruction instruction, Script script, Environment environment)
      {
         #if DEBUG
         Debug.Assert(instruction.OperandCount == 1);
         Debug.Assert(IsVariable(instruction.OperandTypes[0]));
         #endif

         GetValue(script, instruction.OperandTypes[0], instruction.OperandValues[0]).SetValue(new Table());
      }

      private void Divide(Instruction instruction, Script script, Environment environment)
      {
         #if DEBUG
         Debug.Assert(instruction.OperandCount == 3);
         Debug.Assert(IsVariable(instruction.OperandTypes[0]));
         #endif

         Value op1 = GetValue(script, instruction.OperandTypes[1], instruction.OperandValues[1]);
         Value op2 = GetValue(script, instruction.OperandTypes[2], instruction.OperandValues[2]);

         Value dest = GetValue(script, instruction.OperandTypes[0], instruction.OperandValues[0]);
         dest.SetValue(op1);
         
         PrepareForArithmetic(dest);
         dest.Divide(op2);
      }

      private void Equal(Instruction instruction, Script script, Environment environment)
      {
         #if DEBUG
         Debug.Assert(instruction.OperandCount == 3);
         Debug.Assert(IsVariable(instruction.OperandTypes[0]));
         #endif

         Value op1 = GetValue(script, instruction.OperandTypes[1], instruction.OperandValues[1]);
         Value op2 = GetValue(script, instruction.OperandTypes[2], instruction.OperandValues[2]);

         Value dest = GetValue(script, instruction.OperandTypes[0], instruction.OperandValues[0]);
         dest.SetValue(op1);
         
         dest.Equal(op2);
      }

      private void Exponent(Instruction instruction, Script script, Environment environment)
      {
         #if DEBUG
         Debug.Assert(instruction.OperandCount == 3);
         Debug.Assert(IsVariable(instruction.OperandTypes[0]));
         #endif

         Value op1 = GetValue(script, instruction.OperandTypes[1], instruction.OperandValues[1]);
         Value op2 = GetValue(script, instruction.OperandTypes[2], instruction.OperandValues[2]);

         Value dest = GetValue(script, instruction.OperandTypes[0], instruction.OperandValues[0]);
         dest.SetValue(op1);
         
         PrepareForArithmetic(dest);
         dest.Exponent(op2);
      }
      
      private void Function(Instruction instruction, Script script, Environment environment)
      {
         #if DEBUG
         Debug.Assert(instruction.OperandCount == 3);
         Debug.Assert(IsVariable(instruction.OperandTypes[0]));
         Debug.Assert(instruction.OperandTypes[1] == OperandType.IntegerLiteral);
         Debug.Assert(instruction.OperandTypes[2] == OperandType.IntegerLiteral);
         #endif

         Value dest = GetValue(script, instruction.OperandTypes[0], instruction.OperandValues[0]);
         int chunkIndex = instruction.OperandValues[1];
         int paramCount = instruction.OperandValues[2];
         
         Upvalue[] upvalues = script.Chunks[chunkIndex].Upvalues;
         Value[] upvalueCopy = new Value[upvalues.Length];
         
         for (int i = 0; i < upvalues.Length; ++i)
         {
            upvalueCopy[i] = GetValue(script, upvalues[i].Type, upvalues[i].Register);
         }

         dest.SetValue(new Function(script, chunkIndex, paramCount, upvalueCopy));
      }
      
      private void GetGlobal(Instruction instruction, Script script, Environment environment)
      {
         #if DEBUG
         Debug.Assert(instruction.OperandCount == 2);
         Debug.Assert(IsVariable(instruction.OperandTypes[0]));
         #endif

         Value dest = GetValue(script, instruction.OperandTypes[0], instruction.OperandValues[0]);
         Value index = GetValue(script, instruction.OperandTypes[1], instruction.OperandValues[1]);

         dest.SetValue(environment.GlobalTable[index]);
      }
      
      private void GetTable(Instruction instruction, Script script, Environment environment)
      {
         #if DEBUG
         Debug.Assert(instruction.OperandCount == 3);
         Debug.Assert(IsVariable(instruction.OperandTypes[0]));
         Debug.Assert(IsVariable(instruction.OperandTypes[1]));
         #endif

         Value dest = GetValue(script, instruction.OperandTypes[0], instruction.OperandValues[0]);
         Value table = GetValue(script, instruction.OperandTypes[1], instruction.OperandValues[1]);
         Value index = GetValue(script, instruction.OperandTypes[2], instruction.OperandValues[2]);

         dest.SetValue(table.GetTable()[index]);
      }

      private void Jump(Instruction instruction, Script script, Environment environment)
      {
         #if DEBUG
         Debug.Assert(instruction.OperandCount == 1);
         Debug.Assert(instruction.OperandTypes[0] == OperandType.IntegerLiteral);
         #endif
         
         script.Jump(instruction.OperandValues[0]);
      }
      
      private void Length(Instruction instruction, Script script, Environment environment)
      {
         #if DEBUG
         Debug.Assert(instruction.OperandCount == 2);
         Debug.Assert(IsVariable(instruction.OperandTypes[0]));
         #endif

         Value dest = GetValue(script, instruction.OperandTypes[0], instruction.OperandValues[0]);
         Value source = GetValue(script, instruction.OperandTypes[1], instruction.OperandValues[1]);
         
         if (source.Type == ValueType.String)
         {
            dest.SetValue(source.GetString().Length);
         }
         else if (source.Type == ValueType.Table)
         {
            dest.SetValue(source.GetTable().Length);
         }
         else
         {
            throw new Exception(string.Format("Cannot get length of {0} value", source.Type.ToString()));
         }
      }

      private void Less(Instruction instruction, Script script, Environment environment)
      {
         #if DEBUG
         Debug.Assert(instruction.OperandCount == 3);
         Debug.Assert(IsVariable(instruction.OperandTypes[0]));
         #endif

         Value op1 = GetValue(script, instruction.OperandTypes[1], instruction.OperandValues[1]);
         Value op2 = GetValue(script, instruction.OperandTypes[2], instruction.OperandValues[2]);

         Value dest = GetValue(script, instruction.OperandTypes[0], instruction.OperandValues[0]);
         dest.SetValue(op1);
         
         dest.Less(op2);
      }

      private void LessEqual(Instruction instruction, Script script, Environment environment)
      {
         #if DEBUG
         Debug.Assert(instruction.OperandCount == 3);
         Debug.Assert(IsVariable(instruction.OperandTypes[0]));
         #endif

         Value op1 = GetValue(script, instruction.OperandTypes[1], instruction.OperandValues[1]);
         Value op2 = GetValue(script, instruction.OperandTypes[2], instruction.OperandValues[2]);

         Value dest = GetValue(script, instruction.OperandTypes[0], instruction.OperandValues[0]);
         dest.SetValue(op1);
         
         dest.LessEqual(op2);
      }

      private void Modulo(Instruction instruction, Script script, Environment environment)
      {
         #if DEBUG
         Debug.Assert(instruction.OperandCount == 3);
         Debug.Assert(IsVariable(instruction.OperandTypes[0]));
         #endif

         Value op1 = GetValue(script, instruction.OperandTypes[1], instruction.OperandValues[1]);
         Value op2 = GetValue(script, instruction.OperandTypes[2], instruction.OperandValues[2]);

         Value dest = GetValue(script, instruction.OperandTypes[0], instruction.OperandValues[0]);
         dest.SetValue(op1);
         
         PrepareForArithmetic(dest);
         dest.Modulo(op2);
      }

      private void Move(Instruction instruction, Script script, Environment environment)
      {
         #if DEBUG
         Debug.Assert(instruction.OperandCount == 2);
         Debug.Assert(IsVariable(instruction.OperandTypes[0]));
         #endif

         Value dest = GetValue(script, instruction.OperandTypes[0], instruction.OperandValues[0]);
         Value source = GetValue(script, instruction.OperandTypes[1], instruction.OperandValues[1]);

         dest.SetValue(source);
      }

      private void MoveOrSkip(Instruction instruction, Script script, Environment environment)
      {
         #if DEBUG
         Debug.Assert(instruction.OperandCount == 3);
         Debug.Assert(IsVariable(instruction.OperandTypes[0]));
         Debug.Assert(IsVariable(instruction.OperandTypes[1]));
         Debug.Assert(instruction.OperandTypes[2] == OperandType.BooleanLiteral);
         #endif

         Value source = GetValue(script, instruction.OperandTypes[1], instruction.OperandValues[1]);
         Value boolCopy = source.Clone();
         bool test = instruction.OperandValues[2] != 0;
         
         if (boolCopy.GetBoolean() == test)
         {
            Value dest = GetValue(script, instruction.OperandTypes[0], instruction.OperandValues[0]);
            dest.SetValue(source);
         }
         else
         {
            script.Jump(1);
         }
      }

      private void Multiply(Instruction instruction, Script script, Environment environment)
      {
         #if DEBUG
         Debug.Assert(instruction.OperandCount == 3);
         Debug.Assert(IsVariable(instruction.OperandTypes[0]));
         #endif

         Value op1 = GetValue(script, instruction.OperandTypes[1], instruction.OperandValues[1]);
         Value op2 = GetValue(script, instruction.OperandTypes[2], instruction.OperandValues[2]);

         Value dest = GetValue(script, instruction.OperandTypes[0], instruction.OperandValues[0]);
         dest.SetValue(op1);
         
         PrepareForArithmetic(dest);
         dest.Multiply(op2);
      }

      private void Negate(Instruction instruction, Script script, Environment environment)
      {
         #if DEBUG
         Debug.Assert(instruction.OperandCount == 2);
         Debug.Assert(IsVariable(instruction.OperandTypes[0]));
         #endif

         Value op = GetValue(script, instruction.OperandTypes[1], instruction.OperandValues[1]);

         Value dest = GetValue(script, instruction.OperandTypes[0], instruction.OperandValues[0]);
         dest.SetValue(op);
         
         PrepareForArithmetic(dest);
         dest.Negate();
      }

      private void Not(Instruction instruction, Script script, Environment environment)
      {
         #if DEBUG
         Debug.Assert(instruction.OperandCount == 1);
         Debug.Assert(IsVariable(instruction.OperandTypes[0]));
         #endif

         Value dest = GetValue(script, instruction.OperandTypes[0], instruction.OperandValues[0]);
         Value src = GetValue(script, instruction.OperandTypes[1], instruction.OperandValues[1]);
         
         dest.SetValue(src);
         dest.Not();
      }

      private void NotEqual(Instruction instruction, Script script, Environment environment)
      {
         #if DEBUG
         Debug.Assert(instruction.OperandCount == 3);
         Debug.Assert(IsVariable(instruction.OperandTypes[0]));
         #endif

         Value op1 = GetValue(script, instruction.OperandTypes[1], instruction.OperandValues[1]);
         Value op2 = GetValue(script, instruction.OperandTypes[2], instruction.OperandValues[2]);

         Value dest = GetValue(script, instruction.OperandTypes[0], instruction.OperandValues[0]);
         dest.SetValue(op1);
         
         dest.NotEqual(op2);
      }
      
      private void Return(Instruction instruction, Script script, Environment environment)
      {
         #if DEBUG
         Debug.Assert(instruction.OperandCount == 2);
         Debug.Assert(instruction.OperandTypes[0] == OperandType.IntegerLiteral);
         Debug.Assert(instruction.OperandTypes[1] == OperandType.IntegerLiteral);
         #endif
         
         script.PopChunk(instruction.OperandValues[0], instruction.OperandValues[1]);
      }
      
      private void SetGlobal(Instruction instruction, Script script, Environment environment)
      {
         #if DEBUG
         Debug.Assert(instruction.OperandCount == 2);
         #endif

         Value index = GetValue(script, instruction.OperandTypes[0], instruction.OperandValues[0]);
         Value src = GetValue(script, instruction.OperandTypes[1], instruction.OperandValues[1]);

         environment.GlobalTable[index].SetValue(src);
      }
      
      private void SetTable(Instruction instruction, Script script, Environment environment)
      {
         #if DEBUG
         Debug.Assert(instruction.OperandCount == 3);
         Debug.Assert(IsVariable(instruction.OperandTypes[0]));
         #endif

         Value table = GetValue(script, instruction.OperandTypes[0], instruction.OperandValues[0]);
         Value index = GetValue(script, instruction.OperandTypes[1], instruction.OperandValues[1]);
         Value src = GetValue(script, instruction.OperandTypes[2], instruction.OperandValues[2]);

         table.GetTable()[index].SetValue(src);
      }

      private void Subtract(Instruction instruction, Script script, Environment environment)
      {
         #if DEBUG
         Debug.Assert(instruction.OperandCount == 3);
         Debug.Assert(IsVariable(instruction.OperandTypes[0]));
         #endif

         Value op1 = GetValue(script, instruction.OperandTypes[1], instruction.OperandValues[1]);
         Value op2 = GetValue(script, instruction.OperandTypes[2], instruction.OperandValues[2]);

         Value dest = GetValue(script, instruction.OperandTypes[0], instruction.OperandValues[0]);
         dest.SetValue(op1);
         
         PrepareForArithmetic(dest);
         dest.Subtract(op2);
      }
      
      private unsafe float IntToFloat(int value)
      {
         return *(float*)&value;
      }
      
      private Value GetValue(Script script, OperandType type, int index)
      {
         switch (type)
         {
            case OperandType.NilLiteral:     return new Value();
            case OperandType.BooleanLiteral: return new Value(index != 0);
            case OperandType.FloatLiteral:   return new Value(IntToFloat(index));
            case OperandType.IntegerLiteral: return new Value(index);
            case OperandType.StringLiteral:  return new Value(script.GetString(index));
            case OperandType.Value:          return script.GetRegister(index);
            case OperandType.Upvalue:        return script.GetUpvalue(index);
            case OperandType.Parameter:      return script.GetParameter(index);
            case OperandType.Return:         return script.GetReturnValue(index);
            default:                         throw new Exception(string.Format("Unrecognized operand type {0}", (int)type));
         }
      }
      
      private bool IsVariable(OperandType type)
      {
         return type == OperandType.Value ||
                type == OperandType.Upvalue ||
                type == OperandType.Parameter;
      }
      
      private void PrepareForArithmetic(Value value)
      {
         if (value.Type == ValueType.String)
         {
            try
            {
               value.GetInteger();
            }
            catch (FormatException)
            {
               try
               {
                  value.GetFloat();
               }
               catch (FormatException)
               {
                  throw new Exception("Cannot perform arithmetic operation with String type");
               }
            }
         }
         else if (value.Type != ValueType.Integer && value.Type != ValueType.Float)
         {
            throw new Exception(string.Format("Cannot perform arithmetic operation with {0} type", value.Type.ToString()));
         }
      }
      #endregion
   }
}
